from datetime import datetime
import logging
from constants import SUPER_ADMIN_ROLE_NAME, SUPER_ADMIN_ROLE_PERMISSIONS, UUID_NIL
from core.cache.redis_cache import cache_result
from core.entities.account_role import AccountWrap
from extensions.ext_database import db
from libs.helper import GenderStr
from models.account import Account, TenantAccountJoin
from models.role import SysAccountRole, SysMenu, SysMenuType, SysRole, SysRoleMenu
from services.errors.account import AccountNotFoundError
from services.errors.roles import SysMenuNotFoundError, SysRoleNotFoundError, DelBuildInRoleError


class RoleService:

    @staticmethod
    @cache_result(keys=["tenant_id", "role"])
    def get_role_perms(tenant_id: str, role: str) -> list[str]:
        role_obj = db.session.query(SysRole).filter(
            SysRole.tenant_id==tenant_id, SysRole.code == role).first()
        return role_obj.perms if role_obj else []

    @classmethod
    @cache_result(keys=["tenant_id", "roles", "perms"])
    def check_perms(cls, tenant_id: str, roles: list[str], perms: list[str]) -> bool:
        if SUPER_ADMIN_ROLE_NAME in roles:
            return True
        for perm in perms:
            find = False
            for role in roles:
                role_perms = cls.get_role_perms(tenant_id, role)
                if perm in role_perms:
                    find = True
                    break
            if not find:
                return False
        return True

    @staticmethod
    def get_role_list(tenant_id: str, role_id: str | None, name: str | None,code:str|None, menu_id: str | None, page: int = -1, page_size: int = -1) -> dict:
        try:
            query = db.session.query(SysRole).filter(
                SysRole.tenant_id == tenant_id)
            total = 0
            has_more = False
            if role_id:
                query = query.filter(SysRole.id == role_id)
                total = query.count()
            else:
                if name:
                    query = query.filter(SysRole.name.ilike(f"%{name}%"))
                if code:
                    query = query.filter(SysRole.code.ilike(f"%{code}%"))
                if menu_id:
                    query = query.join(SysRoleMenu, (SysRoleMenu.role_id == SysRole.id) & (
                        SysRoleMenu.menu_id == menu_id))
                total = query.count()
                if page and page_size and page_size > 0 and page > 0:
                    query = query.offset((page-1)*page_size).limit(page_size)
                    has_more = total > page*page_size
            data = query.all()
            return {
                "total": total,
                "has_more": has_more,
                "data": data
            }
        except Exception as e:
            logging.error(e)
            return {
                "total": 0,
                "has_more": False,
                "data": [],
                "error": str(e)
            }

    @staticmethod
    def create_role(account: AccountWrap, name,code, remark) -> SysRole:
        try:
            tenant_id = account.current_tenant_id
            role = SysRole(
                tenant_id=tenant_id,  # type: ignore
                name=name,  # type: ignore
                code=code,  # type: ignore
                remark=remark,  # type: ignore
                created_by=account.id,  # type: ignore
            )
            db.session.add(role)
            db.session.commit()
            return role
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            raise e

    @staticmethod
    def update_role(account: AccountWrap, role_id: str, name: str,code:str, remark: str | None, menus: list[str] = []) -> SysRole:
        try:
            tenant_id = account.current_tenant_id
            role = db.session.get(SysRole, role_id)
            if not role:
                raise SysRoleNotFoundError(role_id)
            if role.tenant_id != tenant_id:
                raise SysRoleNotFoundError(role_id)
            role.name = name
            role.code = code
            role.remark = remark
            role.updated_by = account.id
            role.updated_at = datetime.now()
            exit_menus = db.session.query(SysMenu).join(
                SysRoleMenu, (SysRoleMenu.role_id == role_id) & (SysRoleMenu.menu_id == SysMenu.id)).all()
            for menu in exit_menus:
                if menu.id not in menus:
                    db.session.delete(menu)
            for menu_id in menus:
                menu = db.session.get(SysMenu, menu_id)
                if not menu:
                    raise SysMenuNotFoundError(menu_id)
                if menu.tenant_id != tenant_id:
                    raise SysMenuNotFoundError(menu_id)
                db.session.add(SysRoleMenu(
                    role_id=role_id,  # type: ignore
                    menu_id=menu_id  # type: ignore
                ))
            db.session.commit()
            return role
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            raise e

    @staticmethod
    def delete_role(account: AccountWrap, role_id):
        try:
            tenant_id = account.current_tenant_id
            role = db.session.get(SysRole, role_id)
            if not role:
                raise SysRoleNotFoundError(role_id)
            if role.tenant_id != tenant_id:
                raise SysRoleNotFoundError(role_id)
            if role.is_inner:
                raise DelBuildInRoleError()
            db.session.delete(role)
            db.session.commit()
            return role
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            raise e

    @staticmethod
    def get_account_roles(tenant_id: str, account_id: str) -> list[SysRole]:
        try:
            return db.session.query(SysRole).join(SysAccountRole, (SysAccountRole.role_id == SysRole.id) & (SysAccountRole.account_id == account_id) & (SysAccountRole.tenant_id == tenant_id)).order_by(SysRole.is_inner.desc()).all()
        except Exception as e:
            logging.error(e)
            return []

    @staticmethod
    def update_account_roles(tenant_id: str, account_id: str, role_ids: list[str]) -> list[SysRole]:
        try:
            account = db.session.get(Account, account_id)
            if not account:
                raise AccountNotFoundError(account_id)
            account_join = db.session.query(TenantAccountJoin).filter(
                TenantAccountJoin.account_id == account_id, TenantAccountJoin.tenant_id == tenant_id).first()
            if not account_join:
                raise AccountNotFoundError(account_id)
            exist_roles = db.session.query(SysAccountRole).filter(
                SysAccountRole.account_id == account_id).all()
            for account_role in exist_roles:
                if str(account_role.role_id) not in role_ids:
                    db.session.delete(account_role)
            for role_id in role_ids:
                account_role = db.session.query(SysAccountRole).filter(
                    SysAccountRole.account_id == account_id, SysAccountRole.role_id == role_id).first()
                if not account_role:
                    db.session.add(SysAccountRole(
                        account_id=account_id,  # type: ignore
                        role_id=role_id  # type: ignore
                    ))
            db.session.commit()
            return RoleService.get_account_roles(tenant_id, account_id)
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            raise e

    @staticmethod
    def del_account_roles(tenant_id: str, account_id: str, role_ids: list[str]) -> list[SysRole]:
        try:
            account = db.session.get(Account, account_id)
            if not account:
                raise AccountNotFoundError(account_id)
            account_join = db.session.query(TenantAccountJoin).filter(
                TenantAccountJoin.account_id == account_id, TenantAccountJoin.tenant_id == tenant_id).first()
            if not account_join:
                raise AccountNotFoundError(account_id)
            db.session.query(SysAccountRole).filter(
                SysAccountRole.account_id == account_id, SysAccountRole.role_id.in_(role_ids)).delete()
            db.session.commit()
            return RoleService.get_account_roles(tenant_id, account_id)
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            raise e

    @classmethod
    def get_user_info(cls, tenant_id, account_id: str) -> dict:
        try:
            account = db.session.get(Account, account_id)
            if not account:
                raise AccountNotFoundError(account_id)
            roles = db.session.query(SysRole.code).join(SysAccountRole, (SysAccountRole.role_id == SysRole.id) & (
                SysAccountRole.tenant_id == tenant_id)).filter(SysAccountRole.account_id == account_id).all()
            roles = [role.code for role in roles]
            permissions = []
            if SUPER_ADMIN_ROLE_NAME in roles:
                permissions.append(SUPER_ADMIN_ROLE_PERMISSIONS)
            else:
                for role in roles:
                    permissions.append(role.permissions)
                permissions = list(set(permissions))
            return {
                "code": 200,
                "user": {
                    "id": account.id,
                    "name": account.name,
                    "phone": account.phone,
                    "email": account.email,
                    "gender": GenderStr(account.Gender) if account.Gender else None,
                    "avatar": account.avatar,
                    "status": account.status,
                    "interface_language": account.interface_language,
                    "timezone": account.timezone,
                    "score": account.score,
                },
                "roles": roles,
                "permissions": permissions,
            }
        except Exception as e:
            logging.error(e)
            return {"code": 500, "message": "Failed to get account info."}

    @classmethod
    def get_user_routers(cls, tenant_id: str, account_id: str) -> list[dict]:
        roles = db.session.query(SysRole).join(SysAccountRole, (SysAccountRole.role_id == SysRole.id) & (
            SysAccountRole.tenant_id == tenant_id)).filter(SysAccountRole.account_id == account_id).all()
        menu_ids = []
        result = []
        
        for role in roles:
            if role.code ==SUPER_ADMIN_ROLE_NAME:
                result = cls._get_all_routers(tenant_id)
                break
            router_menus = role.routers
            for menu in router_menus:
                if str(menu.id) not in menu_ids:
                    menu_ids.append(str(menu.id))
                    result.append(menu)
        

        return cls.build_menu_tree(result)
    
    @staticmethod
    def build_menu_tree(menus:list[SysMenu])->list[dict]:
        node_map = {str(menu.id):menu.to_dict() for menu in menus}
        tree = []
        for menu in menus:
            menu_id = str(menu.id)
            parent_id = str(menu.parent_id)
            if parent_id == UUID_NIL:
                tree.append(node_map[menu_id])
            else:
                if parent_id in node_map:
                    node_map[parent_id]['children'].append(node_map[menu_id])
        return tree


    @staticmethod
    def _get_all_routers(tenant_id:str)->list[SysMenu]:
        return db.session.query(SysMenu).filter(SysMenu.tenant_id==tenant_id,SysMenu.menu_type.in_(SysMenuType.ROUTER_TYPES())).all()